// Copyright 2011, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jku.mobcomp.helper;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.PreparedQuery;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;


/**
 * This is the utility class for all servlets. It provides method for inserting,
 * deleting, searching the entity from data store. Also contains method for
 * displaying the entity in JSON format.
 * 
 */
public class Utility {

  private static final Logger logger = Logger.getLogger(Utility.class.getCanonicalName());
  private static DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();

/**
 * 
 * @param entity  : entity to be persisted
 */
  public static void persistEntity(Entity entity) {
  	logger.log(Level.INFO, "Saving entity");
  	datastore.put(entity);  	
  }

	/**
	 * Delete the entity from persistent store represented by the key
	 * @param key : key to delete the entity from the persistent store
	 */
  public static void deleteEntity(Key key) {
    logger.log(Level.INFO, "Deleting entity");
    datastore.delete(key);  	
  }
  
  /**
   * Delete list of entities given their keys
   * @param keys
   */
  public static void deleteEntity(final List<Key> keys){
    datastore.delete(new Iterable<Key>() {
		public Iterator<Key> iterator() {
		  return keys.iterator();
		}
      });    
  }

	/**
	 * Search and return the entity from datastore.
	 * @param key : key to find the entity
	 * @return  entity
	 */
 
  public static Entity findEntity(Key key) {
  	logger.log(Level.INFO, "Search the entity");
  	try {	  
  	  return datastore.get(key);
  	} catch (EntityNotFoundException e) {
  	  return null;
  	}
  }
 

	/***
	 * Search entities based on search criteria
	 * @param kind
	 * @param searchBy
	 *            : Searching Criteria (Property)
	 * @param searchFor
	 *            : Searching Value (Property Value)
	 * @return List all entities of a kind from the cache or datastore (if not
	 *         in cache) with the specified properties
	 */
  public static Iterable<Entity> listEntities(String kind, String searchBy,
			String searchFor) {
  	logger.log(Level.INFO, "Search entities based on search criteria");
  	Query q = new Query(kind);
  	if (searchFor != null && !"".equals(searchFor)) {
  	  q.addFilter(searchBy, FilterOperator.EQUAL, searchFor);
  	}
  	PreparedQuery pq = datastore.prepare(q);
  	return pq.asIterable();
  }
  
  
  /**
   * Search entities based on ancestor
   * @param kind
   * @param ancestor
   * @return
   */
  public static Iterable<Entity> listChildren(String kind, Key ancestor) {
  	logger.log(Level.INFO, "Search entities based on parent");
  	Query q = new Query(kind);
  	q.setAncestor(ancestor);
  	q.addFilter(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, ancestor);
  	PreparedQuery pq = datastore.prepare(q);
  	return pq.asIterable();
  }
  
  /**
   * 
   * @param kind
   * @param ancestor
   * @return
   */
  public static Iterable<Entity> listChildKeys(String kind, Key ancestor) {
  	logger.log(Level.INFO, "Search entities based on parent");
  	Query q = new Query(kind);
  	q.setAncestor(ancestor).setKeysOnly();
  	q.addFilter(Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, ancestor);
  	PreparedQuery pq = datastore.prepare(q);
  	return pq.asIterable();
  }

	/**
	 * List the entities in JSON format
	 * 
	 * @param entities  entities to return as JSON strings
	 */
  public static String writeJSON(Iterable<Entity> entities) {
    logger.log(Level.INFO, "creating JSON format object");
  	StringBuilder sb = new StringBuilder();
  	
  	int i = 0;
  	sb.append("{\"data\": [");
  	for (Entity result : entities) {
  	  Map<String, Object> properties = result.getProperties();
  	  sb.append("{");
  	  // use KeyId from property when available; else use keyId from the real Key
  	  String keyIdString = null;
  	  if (result.getProperty(Const.PROP_keyId) != null ){
  		keyIdString = (String) result.getProperty(Const.PROP_keyId) ;
  	  } 
  	  if (keyIdString == null ){
  		keyIdString = Long.toString( result.getKey().getId() );
	  } 
  	  
//	  if (result.getKey().getName() == null)
//		sb.append("\"name\" : \"" + result.getKey().getId() + "\",");
//	  else
//		sb.append("\"name\" : \"" + result.getKey().getName() + "\",");
sb.append("\"keyId\" : \"" + keyIdString + "\",");
//sb.append("\"keyName\" : \"" + result.getKey().getName() + "\",");
//sb.append("\"keyAll\" : \"" + result.getKey().toString() + "\",");

  	  for (String key : properties.keySet()) {
		  	Object value = properties.get(key) ;
		  	// convert Blobs to String
		  	if (value instanceof Blob) {
				try {
					value = new String( ((Blob) value).getBytes() , Const.CHARSET_UTF8);
				} catch (UnsupportedEncodingException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
		    } 
  		sb.append("\"" + key + "\" : \"" + value + "\",");
  	  }
  	  sb.deleteCharAt(sb.lastIndexOf(","));
  	  sb.append("},");
  	  i++;
  	}
  	if (i > 0) {
  	  sb.deleteCharAt(sb.lastIndexOf(","));
  	}  
  	sb.append("]}");
  	return sb.toString();
  }
  
  /**
	 * Retrieves Parent and Child entities into JSON String
	 * 
	 * @param entities
	 *            : List of parent entities
	 * @param childKind
	 *            : Entity type for Child
	 * @param fkName
	 *            : foreign-key to the parent in the child entity
	 * @return JSON string
	 */
  public static String writeJSON(Iterable<Entity> entities, String childKind, String fkName) {
  	logger.log(Level.INFO, "creating JSON format object for parent child relation");
  	StringBuilder sb = new StringBuilder();
  	int i = 0;
  	sb.append("{\"data\": [");
  	for (Entity result : entities) {
  	  Map<String, Object> properties = result.getProperties();
  	  sb.append("{");
  	  if (result.getKey().getName() == null)
  		sb.append("\"name\" : \"" + result.getKey().getId() + "\",");
  	  else
  		sb.append("\"name\" : \"" + result.getKey().getName() + "\",");
  	  for (String key : properties.keySet()) {
  		sb.append("\"" + key + "\" : \"" + properties.get(key) + "\",");
  	  }
  	  Iterable<Entity> child = listEntities(childKind, fkName,
  	  String.valueOf(result.getKey().getId()));
  	  for (Entity en : child) {
  		for (String key : en.getProperties().keySet()) {
  		  sb.append("\"" + key + "\" : \"" + en.getProperties().get(key)+ "\",");
  		}
  	  }
  	  sb.deleteCharAt(sb.lastIndexOf(","));
  	  sb.append("},");
  	  i++;
  	}
  	if (i > 0) {
  	  sb.deleteCharAt(sb.lastIndexOf(","));
  	}  
  	sb.append("]}");
  	return sb.toString();
  }
 
  
	/**
	 * Utility method to send the error back to UI
	 * @param data
	 * @param resp
	 * @throws IOException 
	 */
  public static String getErrorMessage(Exception ex) throws IOException{
    return "Error:"+ex.toString();
  }
 
  /**
   * get DatastoreService instance
   * @return DatastoreService instance
   */
  public static DatastoreService getDatastoreServiceInstance(){
	  return datastore;
  }
  
  /**
   * convert String to Double
   */
  public static Double convertStringToDouble(String input){
	  Double resultDbl = null;
	  try {
		  if (input!=null)
			  resultDbl = Double.valueOf(input);
	  } catch (NumberFormatException nfe) {
	  }
	  return resultDbl;
  }
  
  /**
   * convert String to Long
   */
  public static Long convertStringToLong(String input){
	  Long resultLng = null;
	  try {
		  if (input!=null)
			  resultLng = Long.valueOf(input);
	  } catch (NumberFormatException nfe) {
	  }
	  return resultLng;
  }
  
  /**
   * convert String to Long
   */
  public static Iterable<Entity> putEntityInIterable(Entity entity){
	  LinkedList<Entity> entities = new LinkedList<Entity>();
	  if (entity!=null) {
		  entities.add(entity);
	  } 
	  return entities;
  }
  


  	// Sort the Entietes by a Property
	public static Iterable<Entity> sortEntities(Iterable<Entity> entities,  
			String sortBy, String maxRecords) {
		Iterable<Entity> entitiesResult= entities;   // default return the input-entitiy
		
		// Sorting
		if (entitiesResult!=null && sortBy!=null) {
			Comparator<Entity> comp = null;
			Utility util = new Utility();  // necessary for creating inner Classes (for Comperator)
			if (sortBy.equals(Const.SORTBY_distance) ) {
				comp = util.new CompEntityDistance();
			} 
			if (sortBy.equals(Const.SORTBY_name) ) {
				comp = util.new CompEntityName();
			} 
			if (sortBy.equals(Const.SORTBY_date) ) {
				comp = util.new CompEntityDate();
			} 
			if (sortBy.equals(Const.SORTBY_avgFun) ) {
				comp = util.new CompEntityAvgFun();
			} 
			// a Comperator for the searchBy is available
			if (comp!=null) {
				LinkedList<Entity> entitiesSort= new LinkedList<Entity>();
			  	for (Entity entity : entitiesResult) {
		  			entitiesSort.add(entity);
				} 
				Collections.sort(entitiesSort, comp);
				entitiesResult=entitiesSort;
			} 
		}

		// Limit to maxRecords
		if (entitiesResult!=null && maxRecords!=null) {
			Long maxRecordsLng = Utility.convertStringToLong(maxRecords);
			if (maxRecordsLng!=null) {
				LinkedList<Entity> entitiesLimited= new LinkedList<Entity>();
				int countRec = 0;
			  	for (Entity entity : entitiesResult) {
			  		countRec++;
					if (countRec <= maxRecordsLng.longValue() ) {
						entitiesLimited.add(entity);
					} 
				} 
				entitiesResult=entitiesLimited;
			} 
		}
	  return entitiesResult;
	}

	
	// inner Classes with Comperator for Entities ---  compare Distance
	public class CompEntityDistance implements Comparator<Entity>{
		CompEntityDate compEntityDate = new CompEntityDate();  // used for equal distance
		@Override
		public int compare(Entity ent1, Entity ent2) {
			Double distance1Dbl = (Double) ent1.getProperty(Const.PROP_distance);
			Double distance2Dbl = (Double) ent2.getProperty(Const.PROP_distance);
			double distance1 = Double.MAX_VALUE; // default for putting missing values at the end
			double distance2 = Double.MAX_VALUE; // default for putting missing values at the end
			if (distance1Dbl!=null) {
				distance1 = distance1Dbl.doubleValue();
			} 
			if (distance2Dbl!=null) {
				distance2 = distance2Dbl.doubleValue();
			} 
			int compResult = Double.compare(distance1, distance2);  // ascending
			if (compResult==0) {  // when distance equal, compare date/time
				compResult = compEntityDate.compare(ent1, ent2);
			} 
			return compResult;
		}
	  }
	
	// inner Classes with Comperator for Entities ---  compare avgFun
	public class CompEntityAvgFun implements Comparator<Entity>{
		CompEntityDistance compEntityDistance = new CompEntityDistance();  // used for equal distance
		CompEntityDate compEntityDate = new CompEntityDate();  // used for equal distance
		@Override
		public int compare(Entity ent1, Entity ent2) {
			Double fun1Dbl = (Double) ent1.getProperty(Const.PROP_avgFun);
			Double fun2Dbl = (Double) ent2.getProperty(Const.PROP_avgFun);
			double fun1 = Double.MIN_VALUE; // default for putting missing values at the end -desc!
			double fun2 = Double.MIN_VALUE; // default for putting missing values at the end -desc!
			if (fun1Dbl!=null) {
				fun1 = fun1Dbl.doubleValue();
			} 
			if (fun2Dbl!=null) {
				fun2 = fun2Dbl.doubleValue();
			} 
			int compResult = Double.compare(fun2, fun1);  // descending
			if (compResult==0) {  // when fun equal, compare distance
				compResult = compEntityDistance.compare(ent1, ent2);
			} 
			if (compResult==0) {  // when distance equal, compare date/time
				compResult = compEntityDate.compare(ent1, ent2);
			} 
			return compResult;
		}
	  }

	// inner Classes with Comperator for Entities ---  compare Name
	public class CompEntityName implements Comparator<Entity>{
		@Override
		public int compare(Entity ent1, Entity ent2) {
			String name1 = (String) ent1.getProperty(Const.PROP_name);
			String name2 = (String) ent2.getProperty(Const.PROP_name);
			if (name1==null && name2==null ) {
				return 0;  // both null
			}
			if (name1==null  ) {
				return 1;  // first is bigger; null-value at the end
			}
			if (name2==null  ) {
				return -1;  // second is bigger; null-value at the end
			}
			return name1.compareToIgnoreCase(name2);  // ascending
		}
	  }

	// inner Classes with Comperator for Entities ---  compare Date descending
	public class CompEntityDate implements Comparator<Entity>{
		@Override
		public int compare(Entity ent1, Entity ent2) {
			Date date1 = (Date) ent1.getProperty(Const.PROP_date);
			Date date2 = (Date) ent2.getProperty(Const.PROP_date);
			if (date1==null && date2==null ) {
				return 0;  // both null
			}
			if (date1==null  ) {
				return 1;  // first is bigger; null-value at the end
			}
			if (date2==null  ) {
				return -1;  // second is bigger; null-value at the end
			}
			return date2.compareTo(date1);  // descending
		}
	  }
	
	// End of the inner Classes with Comperator
	
}